/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using System.Text.RegularExpressions;
using System.IO;
using DaveSexton.DocProject.Sandcastle.Resources;

// NOTE: Xml is case-sensitive.
/*
 * In order for this class to be able to parse the document I had to hard-code HTML tags, so I've chosen the casing that was present in the files generated by the Sandcastle December CTP.
 * If casing changes in a subsequent version of Sandcasltle then this class MUST be updated to reflect those changes.  The updates must occur in mulitple places throughout this document.
 */

namespace DaveSexton.DocProject.Sandcastle
{
	public sealed class SandcastleHelpSystem : HelpSystem
	{
		#region Public Properties
		public override IEnumerable<HelpItem> Topics
		{
			get
			{
				XPathNavigator navigator = TableOfContents.CreateNavigator();
				XPathNodeIterator uls = navigator.Select("/HTML/BODY/UL");

				if (uls == null || uls.Count == 0)
					throw new InvalidOperationException(Errors.SandcastleHelpSystemInvalidTOC);

				return new MarshalByRefEnumerable<HelpItem>(GetTopicsRecursive(uls));
			}
		}

		public override IEnumerable<HelpItem> Index
		{
			get
			{
				List<HelpItem> indexItems = new List<HelpItem>();

				foreach (Match match in indexItem.Matches(RawIndex))
				{
					string name = match.Groups["Name"].Value;
					string local = match.Groups["Local"].Value ?? string.Empty;

					indexItems.Add(new HelpItem(name, FormatPath(local)));
				}

				return indexItems.AsReadOnly();
			}
		}

		public string IndexFile
		{
			get
			{
				return indexFile;
			}
		}

		public string TocFile
		{
			get
			{
				return tocFile;
			}
		}
		#endregion

		#region Private / Protected
		private XPathDocument TableOfContents
		{
			get
			{
				if (toc == null)
				{
					lock (sync)
					{
						if (toc == null)
						{
							string source = File.ReadAllText(tocFile);

							source = source.Replace(@"<!DOCTYPE HTML PUBLIC ""-//IETF//DTD HTML/EN"">", @"<?xml version=""1.0"" encoding=""utf-8""?>");

							// give all param elements an ending /> since they don't have a closing tag
							source = topicParam.Replace(source, "$1 />");

							// encode the values of all name and value attributes
							source = topicAttribute.Replace(source, delegate(Match match)
							{
								string attribute = match.Groups["Attribute"].Value;
								string value = match.Groups["Value"].Value;

								if (string.IsNullOrEmpty(value))
									return string.Empty;
								else
                  return string.Format(System.Globalization.CultureInfo.InvariantCulture,
                    "{0}=\"{1}\"", attribute, value.Replace("<", "&lt;"));
							});

							using (StringReader reader = new StringReader(source))
							{
								toc = new XPathDocument(reader);
							}
						}
					}
				}

				return toc;
			}
		}

		private string RawIndex
		{
			get
			{
				if (index == null)
				{
					lock (sync)
					{
						if (index == null)
							index = File.ReadAllText(indexFile);
					}
				}

				return index;
			}
		}

		private readonly Regex indexItem = new Regex(@"<LI>.+? 
			name=""Name"" \s+ value=""(?<Name> .*? )"" .+?
			name=""Local"" \s+ value=""(?<Local> .*? )"" .+?
		(</LI>|<LI>)",
			RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase |
			RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.Compiled);

		private readonly Regex topicParam = new Regex(@"(<param .*? ) > \s* $",
			// NOTE: RegexOptions.Singleline was added because of a bug in Sandcastle's transformations that may write out a topic with carriage returns in its title, 
			// in which case ChmBuilder preserves the whitespace when it creates the .hhc file and we end up with <param name="..." value="{new-line} text {new-line}">
			RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private readonly Regex topicAttribute = new Regex(@"(?<Attribute>name|value)="" (?<Value>.*?) """,
			RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private readonly string tocFile, indexFile, root;
		private volatile XPathDocument toc;
		private volatile string index;
		private static readonly object sync = new object();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleHelpSystem" /> class.
		/// </summary>
		/// <param name="indexFile">Full path and name of the index file for the help system; normally with a <b>.hhk</b> extension.</param>
		/// <param name="tocFile">Full path and name of the table of contents file for the help system; normally with a <b>.hhc</b> extension.</param>
		public SandcastleHelpSystem(string root, string tocFile, string indexFile)
		{
			if (tocFile == null)
				throw new ArgumentNullException("tocFile");

			if (tocFile.Length == 0)
				throw new ArgumentException(Errors.SandcastleHelpSystemEmptyTocFile, "tocFile");

			if (indexFile == null)
				throw new ArgumentNullException("indexFile");

			if (indexFile.Length == 0)
				throw new ArgumentException(Errors.SandcastleHelpSystemEmptyIndexFile, "indexFile");

			this.tocFile = tocFile;
			this.indexFile = indexFile;
			this.root = root;
		}
		#endregion

		#region Methods
		/// <remarks>
		/// The DocSite templates use the file paths as keys between index entries and TOC topics, so they must
		/// be identical in case and format.  This method also makes the specified path relative to the project
		/// directory by prepending the <see cref="root"/> directory, lowered; e.g., help/
		/// </remarks>
		public string FormatPath(string path)
		{
			if (path == null)
				throw new ArgumentNullException("path");

			if (!string.IsNullOrEmpty(root))
				path = Path.Combine(root, path);

			return PathFormat.Clean(path).ToLower(System.Globalization.CultureInfo.CurrentCulture);
		}

		private IEnumerable<HelpItem> GetTopicsRecursive(XPathNodeIterator uls)
		{
			foreach (XPathNavigator ul in uls)
				yield return CreateTocItem(ul.SelectSingleNode("LI"));
		}

		private HelpItem CreateTocItem(XPathNavigator li)
		{
			if (li == null)
				throw new ArgumentNullException(Errors.SandcastleHelpSystemInvalidTOC);

			XPathNavigator name = li.SelectSingleNode(".//param[@name=\"Name\"]/@value");

			if (name == null)
				throw new InvalidOperationException(Errors.SandcastleHelpSystemInvalidTOC);

			XPathNavigator local = li.SelectSingleNode(".//param[@name=\"Local\"]/@value");

			// NOTE: allow empty or missing Local attributes so that certain LI elements can simply act as containers

			return new HelpItem(name.Value,
				(local == null) ? null : FormatPath(local.Value ?? string.Empty),
				GetTopicsRecursive(li.Select("following-sibling::UL")));
		}
		#endregion
	}
}
